< Back
# Spring OAuth2 Client + KeyCloak + React
The scenario: if user hasn't authenticated, user will be redirected to authorization server (Keycloak). After user provided a credential, keycloak response with auth code to browser, then the browser redirect the auth code to a designate callback at api, which allow api to use the auth code to exchange with access token in the back channel.
```plantuml
@startuml
actor User as user
participant "React (Browser)" as react
participant "API (Springboot)" as api
participant KeyCloak as keyclaok
user -> react++: click protected resources
react -> api++:
alt Not Authenticated
api --> react--++
react -> keycloak--++: Redirect
user -> keycloak: Enter credential
keycloak -> react--++: authorization code
react -> api--++: authorization code
api -> keycloak++: exchange token
keycloak -> api--: access token
api -> api: create session
api -> react--: session id
else Authenticated
react -> api: with session id
api <-> react: continue
end
@enduml
```
## Create API
- use Spring init add **web** and **OAuth2 client** as dependencies
- `AppConfig`
```java
package com.kone.sandbox.authlogin.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowCredentials(true);
}
}
```
- `SecurityConfig`
```java
package com.kone.sandbox.authlogin.config;
import com.kone.sandbox.authlogin.handler.AuthenticationHandler;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Slf4j
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthenticationHandler authenticationHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(authenticationHandler)
.and()
.authorizeRequests(a ->
a.antMatchers("/oauth2/authorization/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login()
.defaultSuccessUrl("http://localhost:3000", true);
}
}
```
- `UserDetailController`
```java
package com.kone.sandbox.authlogin.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class UserDetailController {
@GetMapping("/user")
public ResponseEntity<Map<String, Object>> userDetail(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User principal) {
var result = new HashMap<String, Object>() {{
put("principal", authorizedClient.getPrincipalName());
put("principal-name", principal.getName());
put("client-id", authorizedClient.getClientRegistration().getClientId());
}};
return ResponseEntity.ok(result);
}
}
```
- `AuthenticationHandler`
```java
package com.kone.sandbox.authlogin.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@Component
@Slf4j
public class AuthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.addHeader("Access-Control-Allow-Origin", "http://localhost:3000");
response.addHeader("Access-Control-Allow-Credentials", "true");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(new ObjectMapper().writeValueAsString(
Map.of("error", "unauthenticated",
"message", "Probably you haven't login yet."
)
));
log.info("Ended exception handling");
}
}
```
- `application.yml`
```yaml
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: crux-api
client-secret: FsGjfn7QcrOZdXosTjGPhqVik4Y6smz2
authorization-grant-type: authorization_code
provider:
keycloak:
issuer-uri: http://192.168.99.111:8180/auth/realms/wshop
```
## UI
A simple function to call `/user` endpoint could be
```typescript
const fetchUserDetail = () => {
fetch("http://localhost:8080/user", {
credentials: "include",
})
.then((x) => {
if (x.status === 401) {
x.json().then((v) => {
login();
});
return;
}
x.json().then((v) => {
setUserdetail(JSON.stringify(v));
});
})
.catch((err) => {
console.log("error", err);
setUserdetail("ERROR!!");
});
};
```
and use it as
```typescript
<button onClick={fetchUserDetail}>Fetch data</button>
```